비밀번호 저장 (Password Storage)
1. 개요
비밀번호 저장은 사용자가 인증을 위해 설정한 비밀번호를 서버의 데이터베이스에 안전하게 보관하여, 관리자나 외부 공격자가 원본 비밀번호를 알 수 없도록 처리하는 보안 프로세스이다.
현대적인 시스템에서 비밀번호를 평문(Plaintext), 즉 암호화되지 않은 텍스트 그대로 저장하는 것은 매우 위험하다. 데이터베이스가 유출될 경우 모든 사용자의 계정 정보가 즉시 노출되며, 많은 사용자가 여러 서비스에서 동일한 비밀번호를 사용한다는 점(Credential Stuffing 위험) 때문에 2차, 3차 피해로 확산될 가능성이 매우 높기 때문이다.
2. 해시 함수 (Hash Function)
비밀번호 저장의 핵심은 해시 함수를 이용한 단방향 암호화이다. 해시 함수란 임의의 길이를 가진 데이터를 입력받아 고정된 길이의 고유한 문자열(해시값)로 변환하는 함수를 말한다.
해시 함수의 가장 큰 특징은 단방향성(One-way)이다. 즉, 입력값에서 해시값을 계산하는 것은 매우 빠르고 쉽지만, 결과값(해시값)만 가지고 원래의 입력값을 찾아내는 것은 수학적으로 거의 불가능하도록 설계되어 있다. 따라서 서버는 사용자의 비밀번호 원본을 저장하는 대신, 비밀번호의 해시값만을 저장하고 로그인 시 입력받은 값의 해시 결과가 저장된 값과 일치하는지만 확인한다.
2.2 일반 암호화 vs 해시 함수의 차이점
| 구분 |
일반 암호화 (Encryption) |
해시 함수 (Hashing) |
| 방향성 |
양방향 (암호화 $\leftrightarrow$ 복호화) |
단방향 (해시 생성 $\rightarrow$ 복구 불가) |
| 키(Key) 필요성 |
복호화를 위한 키가 반드시 필요함 |
기본적으로 키가 필요 없으나, HMAC 등 특수 목적 시 키 사용 가능 |
| 목적 |
데이터의 기밀 전송 및 복구 |
데이터의 무결성 확인 및 원본 은닉 |
| 결과 길이 |
입력 길이에 따라 가변적 |
입력 길이에 상관없이 항상 고정됨 |
2.3 해시 함수 선택 가이드
비밀번호 저장 목적의 해시 함수를 선택할 때는 다음 기준을 고려해야 한다.
- 충돌 저항성: 서로 다른 입력값이 동일한 해시값을 생성할 확률이 극히 낮아야 한다.
- 연산 비용: 공격자의 무차별 대입 공격을 늦추기 위해 의도적으로 연산 속도가 느려야 한다. (범용 해시 함수인 SHA-256 등은 너무 빨라 부적합함)
- 적응성: 하드웨어 성능 향상에 따라 연산 비용(Cost)을 설정으로 조절할 수 있어야 한다.
3. 키 스트레칭 (Key Stretching)
키 스트레칭이란 해시 함수를 한 번만 적용하는 것이 아니라, 수천 수만 번 반복적으로 적용하여 해시값을 생성하는 기법이다.
3.1 원리와 목적
단순 해싱은 연산 속도가 매우 빨라 공격자가 초당 수억 개의 해시값을 계산해 원문을 찾아낼 수 있다. 키 스트레칭은 hash(hash(hash(...hash(password)...)))와 같이 반복 횟수를 늘림으로써, 정당한 사용자가 로그인할 때는 체감하지 못할 정도의 짧은 지연(예: 0.1초)을 주지만, 수억 번의 시도를 해야 하는 공격자에게는 연산 시간을 기하급수적으로 늘려 공격 비용을 현실적으로 불가능하게 만드는 것이 목적이다.
4. 솔트 (Salt)
단순한 해시 함수만 사용할 경우, 동일한 비밀번호는 항상 동일한 해시값을 생성한다. 이는 공격자가 미리 계산된 해시 테이블을 이용해 비밀번호를 빠르게 찾아낼 수 있게 한다. 이를 방지하기 위해 도입된 개념이 솔트(Salt)이다.
4.1 개념 및 작동 원리
솔트는 비밀번호를 해싱하기 전에 추가하는 무작위 문자열이다. 사용자마다 서로 다른 고유한 솔트를 생성하여 비밀번호에 결합한 뒤 해싱하면, 설령 두 사용자가 동일한 비밀번호(password123)를 사용하더라도 저장되는 최종 해시값은 완전히 달라진다.
4.2 솔트 적용 전후 비교 예시
# 가상의 해시 함수: hash(input)
# 사용자 A와 B가 모두 "password123"를 사용한다고 가정
# [솔트 미적용]
User_A: hash("password123") -> "a5f1...8b2"
User_B: hash("password123") -> "a5f1...8b2" (동일한 결과값 노출)
# [솔트 적용]
# User_A 솔트: "sAlt_789", User_B 솔트: "xYz_123"
User_A: hash("password123" + "sAlt_789") -> "k9p2...m1z"
User_B: hash("password123" + "xYz_123") -> "q7r4...w5v" (서로 다른 결과값)
# ※ 실제 구현 시 솔트는 해시값과 함께 DB에 저장되어야 함
5. 페퍼 (Pepper)
페퍼(Pepper)는 솔트와 유사하지만, 저장 위치와 관리 방식에서 차이가 있는 추가 보안 계층이다.
솔트는 일반적으로 데이터베이스의 사용자 레코드와 함께 저장되지만, 페퍼는 데이터베이스 외부(예: 애플리케이션 설정 파일, HSM, 환경 변수)에 별도로 저장되는 비밀 키이다.
- 작동 방식:
hash(password + salt + pepper)
- 효과: 공격자가 데이터베이스를 탈취하더라도 페퍼 값을 알지 못하면 해시값을 무력화하기 위해 수행하는 연산의 난이도가 기하급수적으로 상승한다. 즉, DB 유출과 서버 설정 파일 유출이라는 두 가지 사고가 동시에 일어나야만 공격이 가능해진다.
6. 주요 공격 기법
저장된 해시값을 무력화하기 위해 공격자들은 다음과 같은 기법을 사용한다.
- 레인보우 테이블(Rainbow Table) 공격: 자주 사용되는 비밀번호와 그에 대응하는 해시값을 미리 계산하여 거대한 테이블로 만들어 둔 뒤, 유출된 해시값과 대조하여 원문을 찾는 공격이다. (솔트 적용 시 무력화됨)
- 무차별 대입 공격(Brute-force): 가능한 모든 문자 조합을 하나씩 대입하여 해시값이 일치할 때까지 반복하는 공격이다.
- 사전 공격(Dictionary Attack): 실제 사람들이 자주 사용하는 단어 목록(사전)을 기반으로 해시값을 대조하는 효율적인 대입 공격이다.
7. 권장 저장 알고리즘
단순한 SHA-256과 같은 범용 해시 함수는 연산 속도가 너무 빨라 무차별 대입 공격에 취약하다. 따라서 비밀번호 저장 전용으로 설계된 적응형 해시 함수(Adaptive Hash Function) 사용이 권장된다.
7.1 알고리즘별 특징 및 비교
| 알고리즘 |
특징 |
보안 수준 |
권장 용도 |
| SHA-256 |
매우 빠른 연산 속도, 범용 해시 |
낮음 (단순 해싱 시) |
파일 무결성 검사 |
| bcrypt |
CPU 연산 비용 조절 가능, 솔트 내장 |
높음 |
일반적인 웹 서비스 |
| scrypt |
메모리 하드웨어 비용 추가 (FPGA/ASIC 방어) |
매우 높음 |
고보안 요구 시스템 |
| Argon2 |
메모리/시간/병렬성 모두 조절 가능 (PHC 우승작) |
최상 |
최신 표준 보안 시스템 |
7.2 알고리즘별 권장 파라미터 설정값
적응형 알고리즘은 하드웨어 성능 발전에 맞춰 연산 비용을 높일 수 있는 파라미터를 제공한다.
| 알고리즘 |
파라미터 |
권장 설정값 (기준) |
설명 |
| bcrypt |
Cost Factor |
$10 \sim 12$ |
$2^{cost}$ 만큼 반복 연산 수행 |
| scrypt |
$N, r, p$ |
$N=16384, r=8, p=1$ |
CPU/메모리 사용량 결정 |
| Argon2id |
$m, t, p$ |
$m=64\text{MB}, t=3, p=4$ |
메모리 크기, 반복 횟수, 병렬성 |
7.3 구현 예제 (Python - Argon2)
최신 표준인 Argon2id를 사용하는 예시이다. (argon2-cffi 라이브러리 기준)
from argon2 import PasswordHasher
# 1. 해셔 초기화 (파라미터는 라이브러리 기본값 또는 설정값 사용)
ph = PasswordHasher()
# 2. 비밀번호 해싱 및 저장
# 내부적으로 무작위 솔트 생성 및 키 스트레칭이 자동으로 수행됨
password = "user_secure_password123"
hashed_password = ph.hash(password)
# 결과값 예: "$argon2id$v=19$m=65536,t=3,p=4$S... (솔트와 파라미터가 포함됨)"
# 3. 비밀번호 검증
user_input = "user_secure_password123"
try:
ph.verify(hashed_password, user_input)
print("인증 성공")
except Exception:
print("인증 실패")
8. 비밀번호 검증 프로세스
사용자가 로그인을 시도할 때 서버에서 수행하는 검증 순서는 다음과 같다.
- 입력: 사용자가
ID와 Password를 입력함.
- 조회: DB에서 해당
ID에 매칭되는 Salt와 Stored_Hash를 가져옴.
- 결합: 입력받은
Password에 조회한 Salt와 서버의 Pepper를 결합함.
- 해싱: 설정된 알고리즘(예: Argon2)과 파라미터를 사용하여 결합된 값을 해싱함.
- 비교: 생성된
New_Hash와 DB의 Stored_Hash가 일치하는지 확인함.
- 결과: 일치하면 인증 성공, 불일치하면 인증 실패 처리.
9. 보안 모범 사례 (Best Practices)
- 고유한 솔트 사용: 모든 사용자에게 동일한 솔트를 사용하지 말고, 사용자별로 무작위로 생성된 고유 솔트를 부여해야 한다.
- 충분한 연산 비용 설정: 로그인 시 약 0.1초 ~ 0.5초 정도의 지연 시간이 발생하도록 Cost 파라미터를 설정하여 공격자의 대입 속도를 늦춰야 한다.
- 안전한 비교 함수 사용: 해시값을 비교할 때
== 연산자 대신 상수 시간 비교(Constant-time comparison) 함수를 사용하여 타이밍 공격(Timing Attack)을 방지해야 한다. (문자열을 앞에서부터 비교하여 일치하지 않는 지점에서 즉시 반환할 경우, 응답 시간 차이로 비밀번호 일부를 유추할 수 있는 공격)
- 주기적인 알고리즘 업데이트: 하드웨어 성능이 향상됨에 따라 기존 알고리즘의 보안성이 낮아질 수 있으므로, 사용자가 로그인할 때 최신 알고리즘으로 해시를 갱신하는 전략을 도입한다.
# 비밀번호 저장 (Password Storage)
## 1. 개요
비밀번호 저장은 사용자가 인증을 위해 설정한 비밀번호를 서버의 데이터베이스에 안전하게 보관하여, 관리자나 외부 공격자가 원본 비밀번호를 알 수 없도록 처리하는 보안 프로세스이다.
현대적인 시스템에서 비밀번호를 **평문(Plaintext)**, 즉 암호화되지 않은 텍스트 그대로 저장하는 것은 매우 위험하다. 데이터베이스가 유출될 경우 모든 사용자의 계정 정보가 즉시 노출되며, 많은 사용자가 여러 서비스에서 동일한 비밀번호를 사용한다는 점(Credential Stuffing 위험) 때문에 2차, 3차 피해로 확산될 가능성이 매우 높기 때문이다.
## 2. 해시 함수 (Hash Function)
비밀번호 저장의 핵심은 **해시 함수**를 이용한 단방향 암호화이다. 해시 함수란 임의의 길이를 가진 데이터를 입력받아 고정된 길이의 고유한 문자열(해시값)로 변환하는 함수를 말한다.
### 2.1 단방향성의 원리
해시 함수의 가장 큰 특징은 **단방향성(One-way)**이다. 즉, 입력값에서 해시값을 계산하는 것은 매우 빠르고 쉽지만, 결과값(해시값)만 가지고 원래의 입력값을 찾아내는 것은 수학적으로 거의 불가능하도록 설계되어 있다. 따라서 서버는 사용자의 비밀번호 원본을 저장하는 대신, 비밀번호의 해시값만을 저장하고 로그인 시 입력받은 값의 해시 결과가 저장된 값과 일치하는지만 확인한다.
### 2.2 일반 암호화 vs 해시 함수의 차이점
| 구분 | 일반 암호화 (Encryption) | 해시 함수 (Hashing) |
| :--- | :--- | :--- |
| **방향성** | 양방향 (암호화 $\leftrightarrow$ 복호화) | 단방향 (해시 생성 $\rightarrow$ 복구 불가) |
| **키(Key) 필요성** | 복호화를 위한 키가 반드시 필요함 | 기본적으로 키가 필요 없으나, HMAC 등 특수 목적 시 키 사용 가능 |
| **목적** | 데이터의 기밀 전송 및 복구 | 데이터의 무결성 확인 및 원본 은닉 |
| **결과 길이** | 입력 길이에 따라 가변적 | 입력 길이에 상관없이 항상 고정됨 |
### 2.3 해시 함수 선택 가이드
비밀번호 저장 목적의 해시 함수를 선택할 때는 다음 기준을 고려해야 한다.
- **충돌 저항성**: 서로 다른 입력값이 동일한 해시값을 생성할 확률이 극히 낮아야 한다.
- **연산 비용**: 공격자의 무차별 대입 공격을 늦추기 위해 의도적으로 연산 속도가 느려야 한다. (범용 해시 함수인 SHA-256 등은 너무 빨라 부적합함)
- **적응성**: 하드웨어 성능 향상에 따라 연산 비용(Cost)을 설정으로 조절할 수 있어야 한다.
## 3. 키 스트레칭 (Key Stretching)
**키 스트레칭**이란 해시 함수를 한 번만 적용하는 것이 아니라, 수천 수만 번 반복적으로 적용하여 해시값을 생성하는 기법이다.
### 3.1 원리와 목적
단순 해싱은 연산 속도가 매우 빨라 공격자가 초당 수억 개의 해시값을 계산해 원문을 찾아낼 수 있다. 키 스트레칭은 `hash(hash(hash(...hash(password)...)))`와 같이 반복 횟수를 늘림으로써, 정당한 사용자가 로그인할 때는 체감하지 못할 정도의 짧은 지연(예: 0.1초)을 주지만, 수억 번의 시도를 해야 하는 공격자에게는 연산 시간을 기하급수적으로 늘려 공격 비용을 현실적으로 불가능하게 만드는 것이 목적이다.
## 4. 솔트 (Salt)
단순한 해시 함수만 사용할 경우, 동일한 비밀번호는 항상 동일한 해시값을 생성한다. 이는 공격자가 미리 계산된 해시 테이블을 이용해 비밀번호를 빠르게 찾아낼 수 있게 한다. 이를 방지하기 위해 도입된 개념이 **솔트(Salt)**이다.
### 4.1 개념 및 작동 원리
솔트는 비밀번호를 해싱하기 전에 추가하는 **무작위 문자열**이다. 사용자마다 서로 다른 고유한 솔트를 생성하여 비밀번호에 결합한 뒤 해싱하면, 설령 두 사용자가 동일한 비밀번호(`password123`)를 사용하더라도 저장되는 최종 해시값은 완전히 달라진다.
### 4.2 솔트 적용 전후 비교 예시
```python
# 가상의 해시 함수: hash(input)
# 사용자 A와 B가 모두 "password123"를 사용한다고 가정
# [솔트 미적용]
User_A: hash("password123") -> "a5f1...8b2"
User_B: hash("password123") -> "a5f1...8b2" (동일한 결과값 노출)
# [솔트 적용]
# User_A 솔트: "sAlt_789", User_B 솔트: "xYz_123"
User_A: hash("password123" + "sAlt_789") -> "k9p2...m1z"
User_B: hash("password123" + "xYz_123") -> "q7r4...w5v" (서로 다른 결과값)
# ※ 실제 구현 시 솔트는 해시값과 함께 DB에 저장되어야 함
```
## 5. 페퍼 (Pepper)
**페퍼(Pepper)**는 솔트와 유사하지만, 저장 위치와 관리 방식에서 차이가 있는 추가 보안 계층이다.
솔트는 일반적으로 데이터베이스의 사용자 레코드와 함께 저장되지만, 페퍼는 **데이터베이스 외부(예: 애플리케이션 설정 파일, HSM, 환경 변수)**에 별도로 저장되는 비밀 키이다.
- **작동 방식**: `hash(password + salt + pepper)`
- **효과**: 공격자가 데이터베이스를 탈취하더라도 페퍼 값을 알지 못하면 해시값을 무력화하기 위해 수행하는 연산의 난이도가 기하급수적으로 상승한다. 즉, DB 유출과 서버 설정 파일 유출이라는 두 가지 사고가 동시에 일어나야만 공격이 가능해진다.
## 6. 주요 공격 기법
저장된 해시값을 무력화하기 위해 공격자들은 다음과 같은 기법을 사용한다.
1. **레인보우 테이블(Rainbow Table) 공격**: 자주 사용되는 비밀번호와 그에 대응하는 해시값을 미리 계산하여 거대한 테이블로 만들어 둔 뒤, 유출된 해시값과 대조하여 원문을 찾는 공격이다. (솔트 적용 시 무력화됨)
2. **무차별 대입 공격(Brute-force)**: 가능한 모든 문자 조합을 하나씩 대입하여 해시값이 일치할 때까지 반복하는 공격이다.
3. **사전 공격(Dictionary Attack)**: 실제 사람들이 자주 사용하는 단어 목록(사전)을 기반으로 해시값을 대조하는 효율적인 대입 공격이다.
## 7. 권장 저장 알고리즘
단순한 SHA-256과 같은 범용 해시 함수는 연산 속도가 너무 빨라 무차별 대입 공격에 취약하다. 따라서 비밀번호 저장 전용으로 설계된 **적응형 해시 함수(Adaptive Hash Function)** 사용이 권장된다.
### 7.1 알고리즘별 특징 및 비교
| 알고리즘 | 특징 | 보안 수준 | 권장 용도 |
| :--- | :--- | :--- | :--- |
| **SHA-256** | 매우 빠른 연산 속도, 범용 해시 | 낮음 (단순 해싱 시) | 파일 무결성 검사 |
| **bcrypt** | CPU 연산 비용 조절 가능, 솔트 내장 | 높음 | 일반적인 웹 서비스 |
| **scrypt** | 메모리 하드웨어 비용 추가 (FPGA/ASIC 방어) | 매우 높음 | 고보안 요구 시스템 |
| **Argon2** | 메모리/시간/병렬성 모두 조절 가능 (PHC 우승작) | 최상 | 최신 표준 보안 시스템 |
### 7.2 알고리즘별 권장 파라미터 설정값
적응형 알고리즘은 하드웨어 성능 발전에 맞춰 연산 비용을 높일 수 있는 파라미터를 제공한다.
| 알고리즘 | 파라미터 | 권장 설정값 (기준) | 설명 |
| :--- | :--- | :--- | :--- |
| **bcrypt** | Cost Factor | $10 \sim 12$ | $2^{cost}$ 만큼 반복 연산 수행 |
| **scrypt** | $N, r, p$ | $N=16384, r=8, p=1$ | CPU/메모리 사용량 결정 |
| **Argon2id** | $m, t, p$ | $m=64\text{MB}, t=3, p=4$ | 메모리 크기, 반복 횟수, 병렬성 |
### 7.3 구현 예제 (Python - Argon2)
최신 표준인 Argon2id를 사용하는 예시이다. (`argon2-cffi` 라이브러리 기준)
```python
from argon2 import PasswordHasher
# 1. 해셔 초기화 (파라미터는 라이브러리 기본값 또는 설정값 사용)
ph = PasswordHasher()
# 2. 비밀번호 해싱 및 저장
# 내부적으로 무작위 솔트 생성 및 키 스트레칭이 자동으로 수행됨
password = "user_secure_password123"
hashed_password = ph.hash(password)
# 결과값 예: "$argon2id$v=19$m=65536,t=3,p=4$S... (솔트와 파라미터가 포함됨)"
# 3. 비밀번호 검증
user_input = "user_secure_password123"
try:
ph.verify(hashed_password, user_input)
print("인증 성공")
except Exception:
print("인증 실패")
```
## 8. 비밀번호 검증 프로세스
사용자가 로그인을 시도할 때 서버에서 수행하는 검증 순서는 다음과 같다.
> 1. **입력**: 사용자가 `ID`와 `Password`를 입력함.
> 2. **조회**: DB에서 해당 `ID`에 매칭되는 `Salt`와 `Stored_Hash`를 가져옴.
> 3. **결합**: 입력받은 `Password`에 조회한 `Salt`와 서버의 `Pepper`를 결합함.
> 4. **해싱**: 설정된 알고리즘(예: Argon2)과 파라미터를 사용하여 결합된 값을 해싱함.
> 5. **비교**: 생성된 `New_Hash`와 DB의 `Stored_Hash`가 일치하는지 확인함.
> 6. **결과**: 일치하면 인증 성공, 불일치하면 인증 실패 처리.
## 9. 보안 모범 사례 (Best Practices)
- **고유한 솔트 사용**: 모든 사용자에게 동일한 솔트를 사용하지 말고, 사용자별로 무작위로 생성된 고유 솔트를 부여해야 한다.
- **충분한 연산 비용 설정**: 로그인 시 약 0.1초 ~ 0.5초 정도의 지연 시간이 발생하도록 Cost 파라미터를 설정하여 공격자의 대입 속도를 늦춰야 한다.
- **안전한 비교 함수 사용**: 해시값을 비교할 때 `==` 연산자 대신 **상수 시간 비교(Constant-time comparison)** 함수를 사용하여 타이밍 공격(Timing Attack)을 방지해야 한다. (문자열을 앞에서부터 비교하여 일치하지 않는 지점에서 즉시 반환할 경우, 응답 시간 차이로 비밀번호 일부를 유추할 수 있는 공격)
- **주기적인 알고리즘 업데이트**: 하드웨어 성능이 향상됨에 따라 기존 알고리즘의 보안성이 낮아질 수 있으므로, 사용자가 로그인할 때 최신 알고리즘으로 해시를 갱신하는 전략을 도입한다.